﻿using Micro.Extensions.Cache.Redis.Extensions;
using Micro.Extensions.Utilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace Micro.Extensions.Cache.Redis
{
    public class RedisClient : ICacheClient, IDisposable
    {
        private readonly ICachingSerializer _cachingSerializer;
        private readonly IDatabase _db;
        private readonly string _prefix;
        private readonly IEnumerable<IServer> _servers;
        private readonly IRedisDatabaseProvider _redisDatabaseProvider;
        private readonly Random _random;

        public IDatabase Db => _db;

        private readonly ILogger _logger;
        public RedisClient(IRedisDatabaseProvider redisDatabaseProvider,
            ICachingSerializer cachingSerializer,
            IOptions<RedisConfig> config,
            ILogger<RedisClient> logger)
        {
            try
            {
                _logger = logger;
                _random = new Random();
                _redisDatabaseProvider = redisDatabaseProvider;
                _cachingSerializer = cachingSerializer;
                _prefix = config.Value.Prefix;
                _db = redisDatabaseProvider.GetDatabase();
                _servers = redisDatabaseProvider.GetServerList();
                _logger?.LogRedis(LogLevel.Information, "初始化 Redis Client 成功!");

                var name = Assembly.GetExecutingAssembly().GetName();
                logger.LogAssemblyInfo(new AssemblyInfo { Name = name.Name, Version = name.Version.ToString(), CodeBase = name.CodeBase });
            }
            catch (Exception e)
            {
                _logger?.LogRedis(LogLevel.Error, "初始化 Redis Client 错误!", e);
                throw e;
            }
        }

#if DEBUG
        public string BuildKey(string key)
#else
        private string BuildKey(string key)
#endif
        {
            string buildKey;
            if (!string.IsNullOrWhiteSpace(_prefix))
                buildKey = $"{_prefix}:{key}";
            else
                buildKey = key;

            if (buildKey.Length > 128)
                _logger.LogRedis(LogLevel.Warning, $"缓存Key:{key}长度超过128位，建议缩减！");

            return buildKey;
        }

        #region get



        public async Task<CacheValue<T>> GetAsync<T>(string key, bool isSliding = false, int slidingExpiredSeconds = 300)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            var cacheKey = BuildKey(key);
            var result = await _db.StringGetAsync(cacheKey);
            _logger.LogRedisDebug(cacheKey, "StringGet", result);

            if (!result.IsNull)
            {
                if (isSliding)
                {
                    ArgCheck.NotNegativeOrZero(slidingExpiredSeconds, nameof(slidingExpiredSeconds));
                    await _db.KeyExpireAsync(cacheKey, TimeSpan.FromSeconds(slidingExpiredSeconds));

                    _logger.LogRedisDebug(cacheKey, "KeyExpire");
                }

                var value = _cachingSerializer.Deserialize<T>(result);
                return new CacheValue<T>(value, true);
            }
            else
                return CacheValue<T>.NoValue;
        }

        public async Task<CacheValue<T>> GetAsync<T>(string key, Func<Task<T>> func, int expiredSeconds, bool isSliding = false)
        {
            return await GetAsync(key, func, TimeSpan.FromSeconds(expiredSeconds), isSliding);
        }

        public async Task<CacheValue<T>> GetAsync<T>(string key, Func<Task<T>> func, TimeSpan expiresIn, bool isSliding = false)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            ArgCheck.NotNegativeOrZero(expiresIn, nameof(expiresIn));
            ArgCheck.NotNull(func, nameof(func));

            var cacheKey = BuildKey(key);
            var mutex = "MUTEX" + cacheKey;
            return await GetByRecurrenceAsync(func, expiresIn, isSliding, cacheKey, mutex);
        }

        private async Task<CacheValue<T>> GetByRecurrenceAsync<T>(Func<Task<T>> func, TimeSpan expiresIn, bool isSliding, string cacheKey, string mutexKey, int deep = 0)
        {
            if (deep > 8) throw new TimeoutException($"缓存加载超过 3600 ms！");

            var result = await _db.StringGetAsync(cacheKey);
            _logger.LogRedisDebug(cacheKey, "StringGet", result);

            if (!result.IsNull)
            {
                if (isSliding)
                {
                    await _db.KeyExpireAsync(cacheKey, expiresIn);
                    _logger.LogRedisDebug(cacheKey, "KeyExpire", expiresIn);
                }

                var value = _cachingSerializer.Deserialize<T>(result);
                return new CacheValue<T>(value, true);
            }


            if (await _db.StringSetAsync(mutexKey, "1", TimeSpan.FromSeconds(60), When.NotExists))
            {
                _logger.LogRedisDebug(mutexKey, "StringSet", "1");
                try
                {
                    var item = await func.Invoke();
                    await _db.StringSetAsync(cacheKey, _cachingSerializer.Serialize(item), expiresIn.Add(TimeSpan.FromMilliseconds(_random.Next(1, 300))));

                    _logger.LogRedisDebug(cacheKey, "StringSet", item);

                    return new CacheValue<T>(item, true);
                }
                catch (Exception e)
                {
                    _logger.LogError(e, "从数据库加载缓存数据失败！");
                    throw;
                }
                finally
                {
                    await _db.KeyDeleteAsync(mutexKey);
                    _logger.LogRedisDebug(mutexKey, "KeyDelete");
                }
            }
            else
            {
                Thread.Sleep(100 * ++deep);
                return await GetByRecurrenceAsync(func, expiresIn, isSliding, cacheKey, mutexKey, deep);
            }
        }

        #endregion

        #region get string

        public async Task<CacheValue<string>> GetStringAsync(string key, bool isSliding = false, int slidingExpiredSeconds = 300)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            var cacheKey = BuildKey(key);
            var result = await _db.StringGetAsync(cacheKey);
            _logger.LogRedisDebug(cacheKey, "StringGet", result);

            if (!result.IsNull)
            {
                if (isSliding)
                {
                    ArgCheck.NotNegativeOrZero(slidingExpiredSeconds, nameof(slidingExpiredSeconds));
                    await _db.KeyExpireAsync(cacheKey, TimeSpan.FromSeconds(slidingExpiredSeconds));

                    _logger.LogRedisDebug(cacheKey, "KeyExpire", slidingExpiredSeconds);
                }

                return new CacheValue<string>(result, true);
            }
            else
                return CacheValue<string>.NoValue;
        }

        public async Task<CacheValue<string>> GetStringAsync(string key, Func<Task<string>> func, int expiredSeconds, bool isSliding = false)
        {
            return await GetStringAsync(key, func, TimeSpan.FromSeconds(expiredSeconds), isSliding);
        }

        public async Task<CacheValue<string>> GetStringAsync(string key, Func<Task<string>> func, TimeSpan expiresIn, bool isSliding = false)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            ArgCheck.NotNegativeOrZero(expiresIn, nameof(expiresIn));
            ArgCheck.NotNull(func, nameof(func));

            var cacheKey = BuildKey(key);
            var mutex = "MUTEX" + cacheKey;
            return await GetStringByRecurrenceAsync(func, expiresIn, isSliding, cacheKey, mutex);
        }

        private async Task<CacheValue<string>> GetStringByRecurrenceAsync(Func<Task<string>> func, TimeSpan expiresIn, bool isSliding, string cacheKey, string mutexKey, int deep = 0)
        {
            if (deep > 8) throw new TimeoutException($"缓存加载超过 3600 ms！");

            var result = await _db.StringGetAsync(cacheKey);
            _logger.LogRedisDebug(cacheKey, "StringGet", result);

            if (!result.IsNull)
            {
                if (isSliding)
                {
                    await _db.KeyExpireAsync(cacheKey, expiresIn);
                    _logger.LogRedisDebug(cacheKey, "KeyExpire", expiresIn);
                }
                return new CacheValue<string>(result, true);
            }

            if (await _db.StringSetAsync(mutexKey, "1", TimeSpan.FromSeconds(60), When.NotExists))
            {
                _logger.LogRedisDebug(mutexKey, "StringSet", "1");
                try
                {
                    var item = await func.Invoke();
                    await _db.StringSetAsync(cacheKey, item, expiresIn);
                    _logger.LogRedisDebug(cacheKey, "StringSet", item);
                    return new CacheValue<string>(item, true);
                }
                catch (Exception e)
                {
                    _logger.LogError(e, "从数据库加载缓存数据失败！");
                    throw;
                }
                finally
                {
                    await _db.KeyDeleteAsync(mutexKey);
                    _logger.LogRedisDebug(mutexKey, "KeyDelete");
                }
            }
            else
            {
                Thread.Sleep(100 * ++deep);
                return await GetStringByRecurrenceAsync(func, expiresIn, isSliding, cacheKey, mutexKey, deep);
            }
        }

        #endregion

        #region set

        public async Task SetAsync<T>(string key, T value, TimeSpan expiresIn)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            ArgCheck.NotNull(value, nameof(value));
            ArgCheck.NotNegativeOrZero(expiresIn, nameof(expiresIn));
            var cacheKey = BuildKey(key);

            await _db.StringSetAsync(cacheKey, _cachingSerializer.Serialize(value), expiresIn.Add(TimeSpan.FromMilliseconds(_random.Next(1, 300))));

            _logger.LogRedisDebug(cacheKey, "StringSet", value, (int)expiresIn.TotalSeconds);
        }

        public async Task SetAsync<T>(string key, T value, int expiredSeconds = 300)
        {
            await SetAsync(key, value, TimeSpan.FromSeconds(expiredSeconds));
        }

        #endregion

        #region set string
        public async Task SetStringAsync(string key, string value, TimeSpan expiresIn)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            ArgCheck.NotNullOrWhiteSpace(value, nameof(value));
            ArgCheck.NotNegativeOrZero(expiresIn, nameof(expiresIn));
            var cacheKey = BuildKey(key);

            await _db.StringSetAsync(cacheKey, value, expiresIn.Add(TimeSpan.FromMilliseconds(_random.Next(1, 300))));
            _logger.LogRedisDebug(cacheKey, "StringSet", value, (int)expiresIn.TotalSeconds);
        }

        public async Task SetStringAsync(string key, string value, int expiredSeconds = 300)
        {
            var expiresIn = TimeSpan.FromSeconds(expiredSeconds);
            await SetStringAsync(key, value, expiresIn.Add(TimeSpan.FromMilliseconds(_random.Next(1, 300))));
        }
        #endregion

        #region remove
        public async Task RemoveAsync(string key)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            var cacheKey = BuildKey(key);

            await _db.KeyDeleteAsync(cacheKey);
            _logger.LogRedisDebug(cacheKey, "KeyDelete");
        }

        public async Task<long> RemoveAsync(string[] keys)
        {
            ArgCheck.NotNullOrWhiteSpaceAndCountGTZero(keys, nameof(keys));

            var buildKeys = keys.Select(s => (RedisKey)BuildKey(s)).ToArray();
            var result = await _db.KeyDeleteAsync(buildKeys);

            if (_logger.IsEnabled(LogLevel.Debug))
                _logger.LogRedisDebug(buildKeys.Select(n => n.ToString()).ToJson(), "KeyDelete", result);

            return result;
        }

        //public async Task<long> RemoveByPrefixAsync(string prefix)
        //{
        //    ArgumentCheck.NotNullOrWhiteSpace(prefix, nameof(prefix));
        //    prefix = this.HandlePrefix(this.BuildKey(prefix));

        //    var redisKeys = this.SearchRedisKeys(prefix);
        //    var result = await _db.KeyDeleteAsync(redisKeys);
        //    this._logger.LogRedisDebug(redisKeys.ToJson(), "KeyDelete", result);
        //    return result;
        //}

        #endregion

        #region exists

        public async Task<bool> ExistsAsync(string key)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));

            var cacheKey = BuildKey(key);
            var result = await _db.KeyExistsAsync(cacheKey);
            _logger.LogRedisDebug(cacheKey, "KeyExists", result);
            return result;
        }

        #endregion

        #region refresh

        public async Task RefreshAsync<T>(string key, T value, TimeSpan expiration)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            ArgCheck.NotNull(value, nameof(value));
            ArgCheck.NotNegativeOrZero(expiration, nameof(expiration));

            await RemoveAsync(key);
            await SetAsync(key, value, expiration);
        }

        public async Task RefreshAsync<T>(string key, T value, int expiredSeconds)
        {
            await RefreshAsync(key, value, TimeSpan.FromSeconds(expiredSeconds));
        }

        #endregion

        #region flush

        //public async Task FlushAsync()
        //{
        //    var prefix = this.HandlePrefix(this.BuildKey(""));
        //    foreach (var server in _servers)
        //    {
        //        var keys = server.Keys(pattern: prefix).ToArray();
        //        await this._db.KeyDeleteAsync(keys);
        //    }
        //}

        #endregion

        #region getoradd
        public async Task<CacheValue<T>> GetOrAddAsync<T>(string key, Func<Task<T>> func, TimeSpan expiresIn, bool isSliding = false)
        {
            return await GetAsync(key, func, expiresIn, isSliding);
        }

        public async Task<CacheValue<T>> GetOrAddAsync<T>(string key, Func<Task<T>> func, int expiredSeconds, bool isSliding = false)
        {
            return await GetAsync(key, func, expiredSeconds, isSliding);
        }

        public async Task<CacheValue<string>> GetOrAddStringAsync(string key, Func<Task<string>> func, TimeSpan expiresIn, bool isSliding = false)
        {
            return await GetStringAsync(key, func, expiresIn, isSliding);
        }

        public async Task<CacheValue<string>> GetOrAddStringAsync(string key, Func<Task<string>> func, int expiredSeconds, bool isSliding = false)
        {
            return await GetStringAsync(key, func, expiredSeconds, isSliding);
        }
        #endregion

        #region private methods
        private string HandlePrefix(string prefix)
        {
            // Forbid
            if (prefix.Equals("*"))
                throw new ArgumentException("the prefix should not equal to *");

            // Don't start with *
            prefix = new System.Text.RegularExpressions.Regex("^\\*+").Replace(prefix, "");

            // End with *
            if (!prefix.EndsWith("*", StringComparison.OrdinalIgnoreCase))
                prefix = string.Concat(prefix, "*");

            return prefix;
        }

#if DEBUG
        public RedisKey[] SearchRedisKeys(string pattern)
#else
        private RedisKey[] SearchRedisKeys(string pattern)
#endif
        {
            var keys = new List<RedisKey>();
            foreach (var server in _servers)
                keys.AddRange(server.Keys(pattern: pattern));

            return keys.Distinct().ToArray();
        }

        #endregion

        public async Task<bool> KeyExpireAsync(string key, TimeSpan expiresIn)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));

            var cacheKey = BuildKey(key);
            var result = await _db.KeyExpireAsync(cacheKey, expiresIn);
            _logger.LogRedisDebug(cacheKey, "KeyExpire", result);
            return result;
        }

        public async Task<long> IncrementAsync(string key, long value = 1)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));

            var cacheKey = BuildKey(key);
            var result = await _db.StringIncrementAsync(cacheKey, value);
            _logger.LogRedisDebug(cacheKey, "StringIncrement", result);
            return result;
        }

        public async Task<long> DecrementAsync(string key, long value = 1)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));

            var cacheKey = BuildKey(key);
            var result = await _db.StringDecrementAsync(cacheKey, value);
            _logger.LogRedisDebug(cacheKey, "StringDecrement", result);
            return result;
        }

        #region Hash集合操作

        public async Task<long> HashIncrementAsync(string key, string field, long value = 1)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            ArgCheck.NotNullOrWhiteSpace(field, nameof(field));

            var cacheKey = BuildKey(key);
            var result = await _db.HashIncrementAsync(cacheKey, field, value);
            _logger.LogRedisDebug(cacheKey, "HashIncrement", new { Field = field, Result = result });
            return result;
        }

        public async Task<long> HashDecrementAsync(string key, string field, long value = 1)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            ArgCheck.NotNullOrWhiteSpace(field, nameof(field));

            var cacheKey = BuildKey(key);
            var result = await _db.HashDecrementAsync(cacheKey, field, value);
            _logger.LogRedisDebug(cacheKey, "HashDecrement", new { Field = field, Result = result });
            return result;
        }

        public async Task<CacheValue<T[]>> HashGetAsync<T>(string key, params string[] fields)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            var cacheKey = BuildKey(key);

            RedisValue[] redisValues = new RedisValue[fields.Length];
            for (int i = 0; i < fields.Length; i++)
            {
                redisValues[i] = fields[i];
            }
            var results = await _db.HashGetAsync(cacheKey, redisValues);
            _logger.LogRedisDebug(cacheKey, "HashGet", new { Fields = fields, Result = results });


            if (results == null || results.Length <= 0)
            {
                return CacheValue<T[]>.NoValue;
            }

            T[] values = new T[results.Length];
            for (int i = 0; i < results.Length; i++)
            {
                values[i] = _cachingSerializer.Deserialize<T>(results[i]);
            }
            return new CacheValue<T[]>(values, true);
        }

        public async Task<CacheValue<KeyValuePair<string, T>[]>> HashGetAllAsync<T>(string key)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            var cacheKey = BuildKey(key);

            var results = await _db.HashGetAllAsync(cacheKey);
            _logger.LogRedisDebug(cacheKey, "HashGetAll", results);

            if (results == null || results.Length <= 0)
            {
                return CacheValue<KeyValuePair<string, T>[]>.NoValue;
            }
            KeyValuePair<string, T>[] keyValuePairs = new KeyValuePair<string, T>[results.Length];
            for (int i = 0; i < results.Length; i++)
            {
                HashEntry entry = results[i];
                keyValuePairs[i] = new KeyValuePair<string, T>(entry.Name, _cachingSerializer.Deserialize<T>(entry.Value));
            }
            return new CacheValue<KeyValuePair<string, T>[]>(keyValuePairs, true);
        }

        public async Task HashSetAsync<T>(string key, KeyValuePair<string, T>[] keyValuePairs)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            ArgCheck.NotNull(keyValuePairs, nameof(keyValuePairs));

            var cacheKey = BuildKey(key);
            HashEntry[] entries = new HashEntry[keyValuePairs.Length];
            for (int i = 0; i < keyValuePairs.Length; i++)
            {
                KeyValuePair<string, T> pair = keyValuePairs[i];
                entries[i] = new HashEntry(pair.Key, _cachingSerializer.Serialize(pair.Value));
            }

            await _db.HashSetAsync(cacheKey, entries);
            _logger.LogRedisDebug(cacheKey, "HashSet", entries);
        }

        public async Task HashRemoveAsync(string key, string field)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            ArgCheck.NotNullOrWhiteSpace(field, nameof(field));

            var cacheKey = BuildKey(key);
            await _db.HashDeleteAsync(cacheKey, field);
            _logger.LogRedisDebug(cacheKey, "HashDelete", field);
        }

        public async Task<bool> HashExistsAsync(string key, string field)
        {
            ArgCheck.NotNullOrWhiteSpace(key, nameof(key));
            ArgCheck.NotNullOrWhiteSpace(field, nameof(field));

            var cacheKey = BuildKey(key);
            var result = await _db.HashExistsAsync(cacheKey, field);
            _logger.LogRedisDebug(cacheKey, "HashExists", field);
            return result;
        }

        #endregion

        public void Dispose()
        {
            _redisDatabaseProvider.Dispose();
        }
    }
}